docs: add callback retention example to Duplicating stubs#172
Conversation
|
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
21b1ebb to
ee055de
Compare
dimitropoulos
left a comment
There was a problem hiding this comment.
I fixed a small leak and got it typechecking and otherwise looks good to go!
example code on the leak
// Failure mode #2: calling setCallback() twice leaks the previous timer AND the
// previous duplicated stub, because `this.timer` / `this.cbfunc` get overwritten.
//
// This uses a real RPC session (MessageChannel) so that `.dup()` and implicit
// param disposal behave exactly as they would over the wire.
//
// Run:
// node fail-2-double-register-leak.ts
//
// Expected output (numbers approximate):
// registered 3 times; server only tracks the last timer + last dup
// after dispose(), orphaned intervals fired ~6 more times in 1s <-- LEAK
//
// A correct implementation (see example.ts) calls a #stop() helper at the top of
// setCallback() so re-registration clears the old timer and disposes the old dup;
// after dispose() the count would be 0.
import { RpcTarget, newMessagePortRpcSession } from 'capnweb';
let liveTimerTicks = 0;
// The OLD, broken server exactly as written in PR #172 (field decls added only
// so Node's type-stripping accepts it; the logic is unchanged).
class Api extends RpcTarget {
cbfunc: any;
timer: any;
async setCallback(cbfunc: any) {
// BUG: no cleanup of any previously-registered callback.
this.cbfunc = cbfunc.dup();
this.timer = setInterval(() => {
liveTimerTicks++;
try { this.cbfunc('tick'); } catch { /* disposed stub */ }
}, 300);
}
[Symbol.dispose]() {
clearInterval(this.timer); // clears ONLY the last timer
this.cbfunc[Symbol.dispose](); // disposes ONLY the last dup
}
}
const { port1, port2 } = new MessageChannel();
const server = new Api();
newMessagePortRpcSession(port2 as any, server);
const api = newMessagePortRpcSession<Api>(port1 as any);
const makeCb = (name: string) => (msg: string) => { void name; void msg; };
await api.setCallback(makeCb('A'));
await api.setCallback(makeCb('B'));
await api.setCallback(makeCb('C')); // 3 registrations => 3 intervals + 3 dups
console.log('registered 3 times; server only tracks the last timer + last dup');
server[Symbol.dispose]();
liveTimerTicks = 0;
await new Promise((r) => setTimeout(r, 1000));
console.log(`after dispose(), orphaned intervals fired ~${liveTimerTicks} more times in 1s <-- LEAK`);
process.exit(0);
commit: |
Invoke the retained callback from a separate RPC method so the example stays within the RPC domain and centers on the dup()/dispose lifecycle. Uses no runtime-specific timer types, so it type-checks on any runtime.
Refs #30.
In #30, @kentonv (member) said "I agree we could use more examples of callbacks." The follow-up comment showed the typical pitfall: a
setCallback(cbfunc)method that storescbfuncand invokes it later from a timer throws "Attempted to use RPC stub after it has been disposed" unless the stub is duplicated with.dup().This adds a short subsection to "Duplicating stubs" in the README with:
.dup(),RpcTargetexample that storescbfunc.dup()and disposes it from[Symbol.dispose],README only. No code changes.